Profile picture

[Docker Compose] 도커 컴포즈로 애플리케이션 Scale out 해보기

JaehyoJJAng2022년 04월 22일

▶︎ 개요

도커 환경에서 Docke Compose를 사용하여 WAS를 scale out하는 상황을 임의로 만들어보고,

nginx를 사용하여 스케일 아웃된 컨테이너들에게로 트래픽이 분산되도록 설정해보자.


▶︎ Application

테스트를 위해 시스템의 호스트명을 출력하는 간단한 애플리케이션 코드를 작성해보자.

해당 애플리케이션은 fastapi로 구동된다.

from fastapi import FastAPI
import socket

app : FastAPI = FastAPI()
@app.get('/hostname')
def get_hostname() -> dict[str,str]:
    hostname = socket.gethostname()
    return {'hostname': hostname}

/hostname 경로로 요청을 받는 app 코드를 작성하였다.


‣ Dockerfile 작성

위 애플리케이션을 도커라이징 하기 위해 도커 파일을 아래와 같이 작성하자. Dockerfile

FROM python:3.10-slim-buster

WORKDIR /usr/src/app

COPY ./requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt

COPY . .
CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "4000"]

‣ Docker Compose 작성

아래와 같은 docker-compose.yaml를 작성하자.

services:
  app:
    build: .
    restart: always
    ports:
      - "4000:4000"
    expose:
      - "4000"
    container_name: scale-app
    volumes:
      - type: bind
        source: "./app"
        target: "/usr/src/app"

모두 작성했다면 아래 명령어를 실행해 app 컨테이너를 실행시키자.

docker-compose up -d --build

▶︎ 애플리케이션 확장

서비스에 접근하는 유동 트래픽이 감당할 수 없을 만큼 늘어 성능을 확장해야 하는 상황이 왔다고 가정해보자.

이 때, 도커 컴포즈의 scale 옵션으로 간단하게 수평적 확장이 가능해진다.

docker-compose up --scale app=2

이 명령을 통해 scale out을 할 경우 자동으로 LB까지 구현 되는데 default 값은 round robin 방식이다.


또는 위 예시처럼 --scale 방법 말고 아래와 같이 docker-compose.yaml 파일에도 명시가 가능하다.

services:
  app:
    # ...
    deploy:
      replicas: 2

deploy.replicas 속성의 값을 2로 주었다. 이 내용은 docker-compose up --scale app=2와 동일하다!


‣ 주의할 점

scale out시에는 docker-compose.yaml에 명시했던 외부 port를 모두 닫아야 한다.

하나의 외부 포트는 하나의 컨테이너만 사용할 수 있는데, 2개의 컨테이너가 같은 host port를 매핑하면 포트 충돌로 인해 정상적으로 실행되지 않는다.

$ docker-compose up -d
Error response from daemon: driver failed programming external connectivity on endpoint blue-green-app-2 (a3eafa456e190f9db22a91cb63e3186aa6afb1dececae16d8633bd4c567331c7): Bind for 0.0.0.0:4000 failed: port is already allocated

위처럼 두번째로 생성된 컨테이너에서 매핑된 호스트 port가 충돌되었다.


또한, container_name 속성을 줬다면 이 속성 또한 삭제해줘야 한다.

$ docker-compose up -d
WARNING: The "app" service is using the custom container name "scale-app". Docker requires each container to have a unique name. Remove the custom name to scale the service.

scale out시에는 container_name 지정이 불가능하다.


정상적으로 포트를 지우고 container_name도 지우고 다시 실행한다면 아래와 같이 2대의 컨테이너가 생성된 것을 볼 수 있을 것이다. image


▶︎ Nginx 설정

먼저 현재 경로에 nginx 폴더를 만들고 하위에 default.conf 파일을 아래와 같이 생성해주자.

# 폴더 생성
mkdir nginx
cd nginx

cat << EOF | tee default.conf
server {
    listen       80;
    server_name  app;

    location / {
        proxy_pass   http://app:4000;
    }
}
EOF

nginx 설정의 경우 아주 간단하게 구성했다.

우리가 만든 app을 등록하고, nginx 80번 포트로 요청이 들어오면 지정한 app으로 요청이 전달되도록 설정을 수정하였다.


‣ Docker Compose 작성

애플리케이션 Docker Compose 작성에서 작성했었던 코드에서 아래 내용을 추가해주자.

services:
  app:
    build: ./app
    restart: always
    expose:
      - "4000"
    volumes:
      - type: bind
        source: "./app"
        target: "/usr/src/app"
    deploy:
      replicas: 2
  
  nginx:
    build: ./nginx
    restart: always
    ports:
      - 80:80
    volumes:
      - type: bind
        source: "nginx/default.conf"
        target: "/etc/nginx/conf.d/default.conf"
    container_name: nginx

다시 한번 docker-compose를 실행시켜보자.

docker-compose up -d --build

image
app 컨테이너가 2개, nginx 컨테이너가 1개 띄워진 것을 볼 수 있다.

nginx의 경우에는 scale out을 적용하지 않았다.


▶︎ 테스트

이제 띄워진 컨테이너에 요청을 보냈을 때 적절히 분산되어 들어가는지 확인해보자.

for i in $(seq 1 10); do curl -w "\n" localhost/hostname; sleep 1; done

image
같은 요청을 보냈지만 각기 다른 hostname이 출력되는 것이 보이는가?

서로 다른 컨테이너로 요청이 정상적으로 분산된 것이다.


Loading script...